Skip to content

[TSK-71] 기존 기필 카테고리 대체과목 보정 로직 수정하여 동일과목 학점 미인정 이슈 해결#310

Open
goldm0ng wants to merge 10 commits intomainfrom
goldm0ng/TSK-71
Open

[TSK-71] 기존 기필 카테고리 대체과목 보정 로직 수정하여 동일과목 학점 미인정 이슈 해결#310
goldm0ng wants to merge 10 commits intomainfrom
goldm0ng/TSK-71

Conversation

@goldm0ng
Copy link
Copy Markdown
Member

@goldm0ng goldm0ng commented Mar 30, 2026

이슈

기존 기필 카테고리 졸업요건 검사 시, 동일과목 커버리지가 낮아 미인정 처리되는 문제가 있었습니다.

[기존 로직]
이수 과목이 required_courses에 존재하는지 확인하고, 없으면 course_replacement 기반으로 대체된 최신 과목을 조회하여 과목명 일치 여부를 판단했습니다.

이수 과목 조회 → required_courses 지정 과목 존재 여부 판단 → 없으면 course_replacement 기반으로 최신 과목 조회 → 과목명 비교

제가 판단한 문제 상황을 예시로 들어보겠습니다.

case1) 사용자가 지정 과목보다 이전 버전의 과목을 들었을 경우

현재 존재하는 최신 과목: A
사용자가 이수한 과목: A' (과거)
required_courses에 정의된 사용자의 학번 및 학과 지정 과목: B (과거 과목)
course_replacement에 정의된 과거 - 최신 과목 매핑: B -> A
해당 과목이 대체된 히스토리: D -> C -> A' -> B -> A

case2) 수강편람 내에 사용자가 이수한 과거 과목에 대한 정보가 없고, 동일과목 목록에는 정의되어 있는 경우

현재 존재하는 최신 과목: A
사용자가 이수한 과목 but 수강편람에 존재하지 않고 동일과목에만 등록된 과목: A'
required_courses에 정의된 사용자의 학번 및 학과 지정 과목: B (과거 과목)
course_replacement에 정의된 과거 - 최신 과목 매핑: B -> A
해당 과목이 대체된 히스토리: D -> C -> B -> A

-> 이 경우는 A'라는 과목이 현재 A라는 과목으로 대체된 과거 과목인데, 수강편람에서 누락된 게 아닐까 추측해봅니다.
혹은 대체된 년도의 수강편람에는 존재하지만, 최신 수강편람에서 누락된 경우로 보입니다.
이런 누락된 케이스가 많이 존재할 거라고 가정하고 작업을 진행해보았습니다.

결과적으로, 기존 로직은 위 두가지 케이스를 커버하지 못했습니다.

원인

기존 course_replacements는 학번 단위로 대체과목을 관리하는 구조였습니다.

이 구조는 다음 문제가 있었습니다.

  • 특정 학번 기준으로만 대체 관계를 관리하기 때문에, 과목 변경 이력 전체를 포괄하기 어렵습니다.
  • 사용자가 required_courses에 적재된 과목보다 더 이전 버전의 과목을 이수한 경우 커버하지 못합니다.
  • 대체과목이 추가될 때마다 학번 별로 모두 반영해야 해서 운영 리소스가 큽니다.

즉, 학번 단위 대체과목 관리 방식 자체가 동일과목/대체과목 판별의 커버리지를 떨어뜨리는 원인이라고 판단했습니다!

작업 내용

1. 졸업요건검사 API - 동일 및 대체과목 통합 관리 엔티티(course_equivalences) 추가

[기존]
대체과목 관리 테이블(course_replacements)에는 학번 별로 대체과목(A→B)만 관리되었습니다.
그러나 동일과목도 함께 관리되어야 합니다.


[개선]
기존 course_replacements는 학번별로 대체과목을 관리했기 때문에, 특정 학번의 학생이 이전 버전의 과목을 이수하는 엣지 케이스에서 커버리지가 낮았습니다. 또한 대체과목이 추가될 때마다 학번마다 모두 추가해줘야 하는 운영 리소스 문제도 존재했습니다.
따라서 학번 단위가 아닌, 과목 자체를 기준으로 동일 및 대체과목 이력을 통합 관리하는 course_equivalences 엔티티를 새로 추가했습니다. course_equivalences는 모든 학번 및 학과에 공통으로 적용되는 대상으로 관리합니다.

[동일과목 및 대체과목을 통합하여 관리하는 방식을 채택한 이유]
동일과목이 대체과목의 상위 개념이라고 판단했습니다.
즉, 동일과목 카테고리 안에 대체과목 카테고리가 포함되는 구조입니다.
이렇게 판단한 이유는 다음과 같습니다.

학사정보시스템 동일과목 목록에는 기존에 저희가 정의했던 대체과목 정보가 모두 포함됩니다.
반면 기존 대체과목 정보에는 동일과목 목록에 있는 과목 중 포함되지 않는 케이스가 실제로 존재합니다. (ex. 물리학및실험입문1)

따라서 동일과목으로 통합 관리하는 것이 커버리지와 운영 효율 모두에서 유리하다고 판단했습니다.

2. 졸업요건검사 API - group_code 기반으로 동일 및 대체과목 판단하도록 방식 변경

[기존] 에는 다음과 같이 deprecated된 대체과목의 이수 여부를 판단하도록 했습니다.

이수 과목 → required_courses 직접 비교 → 없으면 course_replacements 기반 최신 과목 조회 → 과목명 비교


동일 및 대체과목 통합 관리 엔티티(course_equivalences)를 추가함에 따라,
동일 및 대체과목 판단 방식을 변경했습니다.

[개선]

  • required_coursescourse_equivalencegroup_code 칼럼을 추가했습니다.
  • 데이터 적재 방식: 학사정보시스템 동일과목 조회에서 제공하는 CSV 파일을 기반으로 적재했으며, 동일 및 대체과목 관계에 있는 과목들은 동일한 group_code로 묶었습니다.required_courses에 존재하는 과목들과 course_equivalence에 존재하는 과목들도 같은 group_code로 매핑되어 있습니다.

개선된 동일 및 대체과목 판단 -> 동일성 체크 플로우는 다음과 같습니다.

이수 과목 → course_equivalences에서 해당 과목의 group_code 조회 → required_courses에서 해당 group_code로 존재 여부 확인

이를 통해 이수한 과목이 구버전이더라도, group_code가 일치하면 이수한 것으로 판단합니다.

3. 카테고리 별 졸업요건 기준 데이터 조회 API

이 API는 사용자의 입학년도/학과/이수구분별 졸업 기준 데이터를 반환하며, 미이수 과목도 함께 내려줍니다.
course_replacements기반으로 최신 과목으로 보정하는 곳은 두 곳이 존재했습니다.
이를 course_equivalences기반으로 보정하도록 책임을 위임하는 작업을 진행했습니다.

3-1. deprecated 된 과목의 최신 과목 반환 로직 변경

[기존]
카테고리별 졸업요건 기준 데이터를 구성할 때, deprecated된 지정 과목은 course_replacements 기반으로 최신 과목으로 보정하여 반환했습니다.
즉, 기준 데이터에 과거 과목이나 deprecated 과목이 포함되어 있더라도, 응답을 내려주기 전에 course_replacements에서 학번 기준 대체 관계를 조회하여 현재 기준의 과목명/학수번호로 치환하는 방식이었습니다.

흐름으로 보면 아래와 같습니다.

카테고리별 지정 과목 조회 → deprecated 과목 포함 여부 확인 → course_replacements에서 학번 기준 최신 과목 조회 → 최신 과목 정보로 치환하여 응답 반환

이 방식은 학번 단위 대체과목 데이터에 의존하기 때문에, 변경 이력이 길거나 동일과목만 존재하는 케이스에서는 최신 과목 보정이 누락될 수 있었습니다.

[변경]
deprecated 과목의 최신 과목 반환 로직을 course_equivalencesgroup_code 기준으로 변경했습니다.

이제 deprecated된 과목을 직접 대체과목 테이블에서 최신 과목으로 치환하는 것이 아니라,
해당 과목이 속한 group_code를 기준으로 같은 그룹에 속한 현재 과목을 찾아 응답에 내려주도록 변경했습니다.

변경된 흐름은 아래와 같습니다.

카테고리별 지정 과목 조회 → deprecated 과목 여부 확인 → 해당 과목의 group_code 기준으로 같은 그룹의 현재 과목 조회 → 조회된 현재 과목 정보로 응답 반환

즉, 기존처럼 학번별 대체과목 매핑 방식이 아니라, 동일/대체과목 그룹 안에서 현재 기준 과목을 찾아 내려주는 방식으로 변경했습니다.

(중요!!!!!!!!)
⭐️ 최신 과목을 판단하는 방식은,
deprecated된 과목의 group_code와 동일하면서 deprecated 되지 않은 상태(즉, required_courses 테이블의 curiNo가 학수번호로 존재하는 경우)이면 최신과목이라고 판단하고 응답하도록 구현했습니다.

이를 통해 deprecated 과목 응답 보정 로직이 특정 학번의 대체과목 데이터에 종속되지 않고, 동일과목/대체과목 전체 이력을 기준으로 동작하도록 개선했습니다.

3-2. 미이수 과목 판단 로직 변경

[기존]
기존에는 사용자의 이수 과목으로부터 인정 가능한 과목 목록을 만들 때, course_replacements 기반으로 최신 과목을 보정한 뒤 미이수 여부를 판단했습니다.
즉, 사용자가 실제로 이수한 과목들의 학수번호만 바로 비교하는 것이 아니라,
이수 과목명들을 기준으로 course_replacements에서 현재 과목의 대체과목들을 추가 조회하여, 그 결과를 포함한 뒤 미이수 과목을 걸러내는 방식이었습니다.

흐름으로 보면 아래와 같습니다.

사용자의 이수 과목 조회 → 이수 과목의 학수번호/과목명 수집 → course_replacements에서 학번 기준 현재 과목으로 보정 가능한 과목 조회 → 보정된 현재 과목 학수번호를 이수 과목 집합에 추가 → 기준 과목과 비교하여 미이수 여부 판단

이 방식 역시 학번 단위 대체과목 정보에 의존하기 때문에, 미이수 판단이 부정확할 수 있었습니다.

[변경]
미이수 과목 판단도 course_equivalences 기반으로 내려주도록 변경했습니다.

사용자의 이수 과목 학수번호 집합을 먼저 만든 뒤,
course_equivalences에서 같은 group_code에 속한 과목들의 학수번호를 모두 조회하여 인정 가능한 과목들로 포함해 미이수 과목 판단을 하도록 변경했습니다.

변경된 흐름은 아래와 같습니다.

사용자의 이수 과목 조회 → 이수 과목 학수번호 집합 생성 → course_equivalences에서 같은 group_code에 속한 과목 학수번호 전체 조회 → 동일/대체과목으로 인정 가능한 학수번호를 이수 과목 집합에 확장 → 기준 과목과 비교하여 미이수 여부 판단

즉, 기존에는 이수 과목명을 기반으로 최신 과목만을 포함시켰다면,
이수한 과목이 속한 동일 과목 전체를 인정 가능한 과목으로 확장하는 방식으로 변경했습니다.


이를 통해 사용자가 어떤 버전의 과목을 이수했는지와 관계없이, 같은 group_code에 포함된 과목이라면 모두 동일/대체과목으로 인정할 수 있습니다.

검증

  1. 로직 변경에 따른 테스트 코드를 수정했습니다.
  2. 변경 범위가 커서 다음과 같은 케이스에 대한 검증을 진행했습니다.

case1) 지정 과목보다 더 이전 버전의 과목을 이수한 경우

  • 사용자가 required_courses에 적재된 지정 과목보다 더 이전 버전의 과목을 이수했더라도,
    해당 과목이 동일한 group_code에 속해 있으면 정상적으로 이수로 인정되는지 검증했습니다.

[기존] 공업수학1 -> [시나리오 테스트] 공업수학 (옛날 과목)

image image image

-> 정상적으로 학점 인정이 되는 것을 확인했습니다.

case2) 수강편람에는 없지만 동일과목 목록에는 존재하는 과거 과목을 이수한 경우

  • 사용자가 이수한 과목이 수강편람에는 존재하지 않더라도, course_equivalences에 동일과목/대체과목으로 등록되어 있고 동일한 group_code에 속해있다면 정상적으로 이수로 인정되는지 검증했습니다.

[테스트 시나리오] 사용자 정보를 리뷰 사용자 데이터로 설정 후, 기이수 성적표에 <물리학실험및입문1> 과목만을 세팅

image image image

-> 기필 영역에서 해당 과목이 인정되었고, 미이수 과목 추천 목록에도 포함되지 않은 것을 확인했습니다.

case3) 미이수 추천 과목 정상 작동하는지

  • 카테고리별 졸업요건 기준 데이터 조회 시, 사용자의 이수 과목이 동일/대체과목 관계에 있는 과목까지 올바르게 반영되어
    미이수 과목이 정상적으로 필터링되고 추천 과목 목록이 기대한 형태로 내려오는지 검증했습니다.

[테스트 시나리오] 신입생세미나A(과거 과목), 창업과기업가정신1(과거 과목) 두 과목을 기이수 성적표에서 삭제

image

-> 미이수 과목 추천에서 과거 과목 정보가 아닌, 최신 과목 정보로 반환되는 것을 확인했습니다.

고민 지점과 리뷰 포인트

  1. 가독성 괜찮나요!?
  2. 다른 엣지 케이스가 있을까요?
  3. 현재 구조에서 우려되는 점이나, 운영/데이터 정합성 측면에서 위험해보이는 부분이 있다면 리뷰 부탁드립니다!
  4. 최신 과목 판단 방식에 대해

처음에는 course_equivalences를 히스토리 순서대로 적재한 뒤, 같은 group_code 내에서 맨 앞/맨 뒤 값을 최신 과목으로 판단하는 방식을 고려했습니다. 하지만 이 방식은 데이터 적재 순서에 의존하고, 동일과목/대체과목 이력이 항상 일관된 순서로 관리된다고 보장하기 어려워 적용하지 않았습니다!! 결정적인 이유는 학사정보시스템 동일과목 조회 데이터에서의 과목 순서가 변경이력대로 조회되지 않다는 점이었습니당.
대신 현재는 과목 변경 순서에 의존하지 않고 required_coursesgroup_code를 기준으로 판단하도록 구성했습니다.
또한 데이터 상에서, required_coursesgroup_code가 없는 경우는 동일/대체과목이 없는 유일 과목이라고 판단했으며, 로직상 required_courses를 먼저 비교한 뒤 course_equivalences를 확인하기 때문에 큰 문제는 없다고 생각합니다!

제가 판단한 방식이 적절한지 의견 부탁드립니다!!


TODO (현재 구현된 방향으로 머지가 된다면)

  1. 엔티티 생성으로 인한 validator 추가로, yml 파일을 수정할 예정입니다.
  2. 새로 추가한 엔티티의 db table을 create할 예정입니다.
  3. 현재는 졸업요건 기준 데이터 적재 작업을 저의 개인 계정 구글 시트로 진행 후, 테스트 해본 상황입니다. 이를 서비스 계정 구글 시트에 옮기고, DB 동기화할 예정입니다. (리뷰 및 논의 후 다른 방향성으로 구현될 경우를 대비하여 안전하게 위와 같이 진행했습니다.)

@goldm0ng goldm0ng self-assigned this Mar 30, 2026
@auto-assign auto-assign bot requested review from 2Jin1031, boyekim and haeyoon1 March 30, 2026 10:32
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 30, 2026

Test Results

188 tests   188 ✅  26s ⏱️
 43 suites    0 💤
 43 files      0 ❌

Results for commit de8ae84.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e272f14ca

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +10 to +13
select e.groupCode from CourseEquivalence e
where e.curiNo = :curiNo
""")
Optional<String> findGroupCodeByCuriNo(String curiNo);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce unique curi_no when reading equivalence group

findGroupCodeByCuriNo assumes a single row, but course_equivalences is populated from a sheet that is only string-validated (no uniqueness check on curi_no), so duplicate rows for the same course code will make this query return multiple results and throw IncorrectResultSizeDataAccessException at runtime during academic-basic evaluation. This should either be made tolerant (distinct/first) or guarded by validation/constraints so one bad sheet row does not break graduation checks.

Useful? React with 👍 / 👎.

graduationSheetTable.getString(row, "curi_no"),
graduationSheetTable.getString(row, "curi_nm"),
graduationSheetTable.getString(row, "alt_group"),
graduationSheetTable.getString(row, "group_code"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject required-course rows missing group_code

This sync now persists group_code and the new findRequiredCourseInGroup logic depends on it, but required-courses validation still does not require that column/value; if the sheet is not updated (or cells are blank), rows are saved with null group codes and group-based matching always misses, causing equivalent/replacement academic-basic courses to be incorrectly treated as not required instead of failing fast during sync.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant